﻿///////////////////////////////////////////////////////////////////////////////
//
// This file is part of the BassDrop project
//   https://code.google.com/p/bass-drop/
//
//
// Creator:	Gergely Krajcsovszki (krajcsovszkig@hotmail.com)
// Date:	2013-09-21 16:10
//
//
// Copyright © 2013-2014. Gergely Krajcsovszki
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//   http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
///////////////////////////////////////////////////////////////////////////////
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using BassDrop.Core.Common.Data.IO;
using BassDrop.Core.Common.Data.IO.Data;
using BassDrop.Core.Common.Exceptions;
using BassDrop.Core.Common.General.Collections;
using BassDrop.Core.Common.Media.Library;
using BassDrop.Core.Common.Media.Playlists;
using BassDrop.Core.Common.Plugins;
using BassDrop.Core.Common.Services;

namespace BassDrop.Core.Common.Media
{
	/// <summary>
	/// An audio track that can be played
	/// </summary>
	public class MediaTrack : INotifyPropertyChanged
	{

		#region Types

		/// <summary>
		/// Metadata about a MediaTrack
		/// </summary>
		public class Metadata : INotifyPropertyChanged
		{
			#region Properties

			/// <summary>
			/// Title of the track
			/// 
			/// Null if unknown
			/// </summary>
			private string m_title;
			/// <summary>
			/// Title of the track
			/// 
			/// Null if unknown
			/// </summary>
			public string Title
			{
				get
				{
					return m_title;
				}

				set
				{
					m_title = value;

					if ( PropertyChanged != null )
					{
						PropertyChanged( this, new PropertyChangedEventArgs( "Title" ) );
					}
				}
			}

			/// <summary>
			/// Artist of the track
			/// 
			/// Null if unknown
			/// </summary>
			public string m_artist;
			/// <summary>
			/// Artist of the track
			/// 
			/// Null if unknown
			/// </summary>
			public string Artist
			{
				get
				{
					return m_artist;
				}

				set
				{
					m_artist = value;

					if ( PropertyChanged != null )
					{
						PropertyChanged( this, new PropertyChangedEventArgs( "Artist" ) );
					}
				}
			}

			/// <summary>
			/// Album of the track
			/// 
			/// Null if unknown
			/// </summary>
			public string m_album;
			/// <summary>
			/// Album of the track
			/// 
			/// Null if unknown
			/// </summary>
			public string Album
			{
				get
				{
					return m_album;
				}

				set
				{
					m_album = value;

					if ( PropertyChanged != null )
					{
						PropertyChanged( this, new PropertyChangedEventArgs( "Album" ) );
					}
				}
			}

			/// <summary>
			/// Album artist of the track
			/// 
			/// Null if unknown
			/// </summary>
			public string m_albumArtist;
			/// <summary>
			/// Album artist of the track
			/// 
			/// Null if unknown
			/// </summary>
			public string AlbumArtist
			{
				get
				{
					return m_albumArtist;
				}

				set
				{
					m_albumArtist = value;

					if ( PropertyChanged != null )
					{
						PropertyChanged( this, new PropertyChangedEventArgs( "AlbumArtist" ) );
					}
				}
			}

			/// <summary>
			/// The index of the track on the album
			/// 
			/// Null if unknown
			/// </summary>
			public int? m_trackIndex;
			/// <summary>
			/// The index of the track on the album
			/// 
			/// Null if unknown
			/// </summary>
			public int? TrackIndex
			{
				get
				{
					return m_trackIndex;
				}

				set
				{
					m_trackIndex = value;

					if ( PropertyChanged != null )
					{
						PropertyChanged( this, new PropertyChangedEventArgs( "TrackIndex" ) );
					}
				}
			}

			/// <summary>
			/// Release year of the track
			/// 
			/// Null if unknown
			/// </summary>
			public int? m_year;
			/// <summary>
			/// Release year of the track
			/// 
			/// Null if unknown
			/// </summary>
			public int? Year
			{
				get
				{
					return m_year;
				}

				set
				{
					m_year = value;

					if ( PropertyChanged != null )
					{
						PropertyChanged( this, new PropertyChangedEventArgs( "Year" ) );
					}
				}
			}

			/// <summary>
			/// Genre of the track
			/// 
			/// Null if unknown
			/// </summary>
			public string n_genre;
			/// <summary>
			/// Genre of the track
			/// 
			/// Null if unknown
			/// </summary>
			public string Genre
			{
				get
				{
					return n_genre;
				}

				set
				{
					n_genre = value;

					if ( PropertyChanged != null )
					{
						PropertyChanged( this, new PropertyChangedEventArgs( "Genre" ) );
					}
				}
			}

			/// <summary>
			/// Uri to the album art
			/// 
			/// Null if unknown
			/// </summary>
			public Uri m_albumArtLocation;
			/// <summary>
			/// Uri to the album art
			/// 
			/// Null if unknown
			/// </summary>
			public Uri AlbumArtLocation
			{
				get
				{
					return m_albumArtLocation;
				}

				set
				{
					m_albumArtLocation = value;

					if ( PropertyChanged != null )
					{
						PropertyChanged( this, new PropertyChangedEventArgs( "AlbumArtLocation" ) );
					}
				}
			}

			// TODO: additional metadata
			/*
			 * tempo
			 * key
			 * composer
			 * publisher
			 * tags
			 * comments
			 * etc.
			 * 
			 * MergeInt frissíteni
			 */

			/// <summary>
			/// Rating of the track in percents
			/// 
			/// Null if not rated
			/// </summary>
			public int? m_rating;
			/// <summary>
			/// Rating of the track in percents
			/// 
			/// Null if not rated
			/// </summary>
			public int? Rating
			{
				get
				{
					return m_rating;
				}

				set
				{
					m_rating = value;

					if ( PropertyChanged != null )
					{
						PropertyChanged( this, new PropertyChangedEventArgs( "Rating" ) );
					}
				}
			}

			/// <summary>
			/// Play history of the track
			/// </summary>
			private SuperCollection<DateTime> m_plays = new SuperCollection<DateTime>( true );
			/// <summary>
			/// Play history of the track
			/// </summary>
			public SuperCollection<DateTime> Plays
			{
				get
				{
					return m_plays;
				}

				set
				{
					if ( value == null )
					{
						m_plays.Clear();
					}
					else
					{
						m_plays.CollectionChanged -= m_plays_CollectionChanged;

						m_plays = value;

						m_plays.CollectionChanged -= m_plays_CollectionChanged;
						m_plays.CollectionChanged += m_plays_CollectionChanged;
						m_plays.Sorted = true;
					}

					if ( PropertyChanged != null )
					{
						PropertyChanged( this, new PropertyChangedEventArgs( "Plays" ) );
					}
				}
			}

			/// <summary>
			/// Fires a PropertyChanged event when the Plays collection changes
			/// </summary>
			/// <param name="sender"></param>
			/// <param name="e"></param>
			void m_plays_CollectionChanged( object sender, NotifyCollectionChangedEventArgs e )
			{
				PropertyChanged( this, new PropertyChangedEventArgs( "Plays" ) );
			}

			/// <summary>
			/// The date the track was added to the media library
			/// 
			/// Null if not added
			/// </summary>
			public DateTime? m_dateAdded;
			/// <summary>
			/// The date the track was added to the media library
			/// 
			/// Null if not added
			/// </summary>
			public DateTime? DateAdded
			{
				get
				{
					return m_dateAdded;
				}

				set
				{
					m_dateAdded = value;

					if ( PropertyChanged != null )
					{
						PropertyChanged( this, new PropertyChangedEventArgs( "DateAdded" ) );
					}
				}
			}

			/// <summary>
			/// Pretty format name of the track
			/// 
			/// Null if unknown
			/// </summary>
			public string m_format;
			/// <summary>
			/// Pretty format name of the track
			/// 
			/// Null if unknown
			/// </summary>
			public string Format
			{
				get
				{
					return m_format;
				}

				set
				{
					m_format = value;

					if ( PropertyChanged != null )
					{
						PropertyChanged( this, new PropertyChangedEventArgs( "Format" ) );
					}
				}
			}

			/// <summary>
			/// Bitrate of the track (in bits per second)
			/// 
			/// Null if unknown
			/// </summary>
			public int? m_bitrate;
			/// <summary>
			/// Bitrate of the track (in bits per second)
			/// 
			/// Null if unknown
			/// </summary>
			public int? Bitrate
			{
				get
				{
					return m_bitrate;
				}

				set
				{
					m_bitrate = value;

					if ( PropertyChanged != null )
					{
						PropertyChanged( this, new PropertyChangedEventArgs( "Bitrate" ) );
					}
				}
			}

			/// <summary>
			/// The length of the track in milliseconds
			/// 
			/// Null if the length is indeterminate
			/// </summary>
			public long? m_length;
			/// <summary>
			/// The length of the track in milliseconds
			/// 
			/// Null if the length is indeterminate
			/// </summary>
			public long? Length
			{
				get
				{
					return m_length;
				}

				set
				{
					m_length = value;

					if ( PropertyChanged != null )
					{
						PropertyChanged( this, new PropertyChangedEventArgs( "Length" ) );
					}
				}
			}

			#endregion Properties

			/// <summary>
			/// Event for changes to any property
			/// </summary>
			public event PropertyChangedEventHandler PropertyChanged;

			/// <summary>
			/// Merges another Metadata object into this one (fills in nulls)
			/// </summary>
			/// <param name="other"></param>
			public Metadata Merge( Metadata other )
			{
				// Copy this
				Metadata ret = (Metadata) MemberwiseClone();

				ret.Plays = new SuperCollection<DateTime>( Plays, true );

				if ( other == null )
				{
					return ret;
				}

				// Merge properties
				if ( ret.Album == null )
				{
					ret.Album = other.Album;
				}

				if ( ret.AlbumArtist == null )
				{
					ret.AlbumArtist = other.AlbumArtist;
				}

				if ( ret.AlbumArtLocation == null )
				{
					ret.AlbumArtLocation = other.AlbumArtLocation;
				}

				if ( ret.Artist == null )
				{
					ret.Artist = other.Artist;
				}

				if ( ret.Bitrate == null )
				{
					ret.Bitrate = other.Bitrate;
				}

				if ( ret.DateAdded == null )
				{
					ret.DateAdded = other.DateAdded;
				}

				if ( ret.Format == null )
				{
					ret.Format = other.Format;
				}

				if ( ret.Genre == null )
				{
					ret.Genre = other.Genre;
				}

				if ( ret.Length == null )
				{
					ret.Length = other.Length;
				}

				ret.Plays.AddRange( other.Plays );

				if ( ret.Rating == null )
				{
					ret.Rating = other.Rating;
				}

				if ( ret.Title == null )
				{
					ret.Title = other.Title;
				}

				if ( ret.TrackIndex == null )
				{
					ret.TrackIndex = other.TrackIndex;
				}

				if ( ret.Year == null )
				{
					ret.Year = other.Year;
				}

				return ret;
			}


		}

		#endregion Types

		#region Properties

		/// <summary>
		/// The location of this media file
		/// </summary>
		public ResourceLocation MediaLocation
		{
			get;
			private set;
		}

		/// <summary>
		/// Path to the file in the system
		/// </summary>
		public Uri FilePath
		{
			get
			{
				return MediaIO.GetValidAbsoluteUriFrom( MediaLocation ); // TODO: cachelni
			}
		}

		/// <summary>
		/// Gets the name of the file (without extension)
		/// </summary>
		public string FileName
		{
			get
			{
				if ( FilePath == null )
				{
					return string.Empty;
				}
				else
				{
					string nameWithExtension = ServiceCenter.FileHandler.GetFileName( FilePath.ToString() );
					int extensionOffset = nameWithExtension.LastIndexOf( '.' );
					extensionOffset = (extensionOffset < 0 ? nameWithExtension.Length : extensionOffset);

					return nameWithExtension.Substring( 0, extensionOffset );
				}
			}
		}
		/// <summary>
		/// Gets the extension of the file (with a leading dot)
		/// </summary>
		public string FileExtension
		{
			get
			{
				if ( FilePath == null )
				{
					return string.Empty;
				}
				else
				{
					return ServiceCenter.FileHandler.GetExtension( FilePath.ToString() ).ToLower();
				}
			}
		}

		/// <summary>
		/// Metadata for the track
		/// </summary>
		private Metadata m_trackMetadata;
		/// <summary>
		/// Metadata for the track
		/// </summary>
		public Metadata TrackMetadata
		{
			get
			{
				return m_trackMetadata;
			}

			private set
			{
				if ( m_trackMetadata != null )
				{
					m_trackMetadata.PropertyChanged -= m_trackMetadata_PropertyChanged;
				}

				m_trackMetadata = value;

				if ( m_trackMetadata != null )
				{
					m_trackMetadata.PropertyChanged -= m_trackMetadata_PropertyChanged;
					m_trackMetadata.PropertyChanged += m_trackMetadata_PropertyChanged;
				}

				OnPropertyChanged( new PropertyChangedEventArgs( "TrackMetadata" ) );
			}
		}

		private long? m_libraryId;

		/// <summary>
		/// The unique ID of this item in the media library.<para/>
		/// Id >= 1001<para/>
		/// Tracks that are not in the library have a null ID.
		/// </summary>
		public long? LibraryId
		{
			get
			{
				return m_libraryId;
			}

			protected set
			{
				if ( m_libraryId != value )
				{
					m_libraryId = value;
					OnPropertyChanged( new PropertyChangedEventArgs( "LibraryId" ) );
				}
			}
		}

		private LibraryEntry m_libraryEntry;

		/// <summary>
		/// Entry for the track in the media library
		/// </summary>
		public LibraryEntry LibraryEntry
		{
			get
			{
				return m_libraryEntry;
			}
			private set
			{
				if ( m_libraryEntry != value )
				{
					m_libraryEntry = value;
					LibraryId = m_libraryEntry.Id;
				}
			}
		}

		/// <summary>
		/// Gets the length of the track in milliseconds
		/// 
		/// Null if the length is indeterminate
		/// </summary>
		public long? Length
		{
			get
			{
				if ( TrackMetadata == null )
				{
					return null;
				}
				else
				{
					return TrackMetadata.Length;
				}
			}
		}

		private Exception m_error;

		/// <summary>
		/// Error occured while handling the item
		/// </summary>
		public Exception Error
		{
			get
			{
				return m_error;
			}

			set
			{
				if ( m_error != value )
				{
					m_error = value;
					OnPropertyChanged( new PropertyChangedEventArgs( "Error" ) );
				}
			}
		}

		private MediaDecoderBase m_mediaPlayer;

		/// <summary>
		/// A mediaplayer that can play this item
		/// 
		/// Null if none found
		/// </summary>
		public MediaDecoderBase MediaPlayer
		{
			get
			{
				return m_mediaPlayer;
			}

			protected set
			{
				if ( m_mediaPlayer != value )
				{
					m_mediaPlayer = value;
					OnPropertyChanged( new PropertyChangedEventArgs( "MediaPlayer" ) );
				}
			}
		}

		private bool m_isInitialized = false;

		/// <summary>
		/// Is the track initialized
		/// </summary>
		public bool IsInitialized
		{
			get
			{
				return m_isInitialized;
			}
			set
			{
				if ( m_isInitialized != value )
				{
					m_isInitialized = value;
					OnPropertyChanged( new PropertyChangedEventArgs( "IsInitialized" ) );
				}
			}
		}


		#endregion Properties

		#region Events

		/// <summary>
		/// Event fired when a property of the item or the referenced media track changes
		/// </summary>
		public event PropertyChangedEventHandler PropertyChanged;

		/// <summary>
		/// Fires a PropertyChanged event
		/// </summary>
		/// <param name="e"></param>
		protected void OnPropertyChanged( PropertyChangedEventArgs e )
		{
			if ( PropertyChanged != null )
			{
				PropertyChanged( this, e );
			}
		}

		/// <summary>
		/// Event handler for Metadata events - forwards them to subscribers on the MediaTrack
		/// PropertyChanged event
		/// </summary>
		/// <param name="sender"></param>
		/// <param name="e"></param>
		void m_trackMetadata_PropertyChanged( object sender, PropertyChangedEventArgs e )
		{
			OnPropertyChanged( e );
		}

		#endregion Events

		#region Construction and initialization

		/// <summary>
		/// Constructs a new media track for a file in the system.
		/// 
		/// Asynchronously initializes the other properties.
		/// 
		/// Uses SuperCollection's DefaultOwnerContext for the initialization
		/// </summary>
		/// <param name="mediaLocation">the location of this media file</param>
		private MediaTrack( ResourceLocation mediaLocation )
		{
			MediaLocation = mediaLocation;
			SuperCollectionStaticNonGeneric.DefaultOwnerContext.Post( async ( o ) => await Init(), null );
		}

		/// <summary>
		/// The loaded tracks that should be playable on this system.
		/// Key is their absolute URI (one track may appear for multiple keys)
		/// </summary>
		private static Dictionary<Uri, MediaTrack> m_validLoadedTracks = new Dictionary<Uri, MediaTrack>();

		/// <summary>
		/// The loaded tracks that are not playable on this system, because no valid absolute path have been found for them.
		/// Key is their ResourceLocation
		/// </summary>
		private static Dictionary<ResourceLocation, MediaTrack> m_invalidLoadedTracks = new Dictionary<ResourceLocation, MediaTrack>();

		/// <summary>
		/// Returns the MediaTrack for the passed location if it exists<para/>
		/// Else, creates a new one
		/// </summary>
		/// <param name="mediaLocation"></param>
		/// <returns></returns>
		public static MediaTrack ForLocation( ResourceLocation mediaLocation )
		{
			MediaTrack track = null;

			foreach ( var item in MediaIO.GetValidAbsoluteUrisFrom( mediaLocation ) )
			{
				if ( m_validLoadedTracks.ContainsKey( item ) )
				{
					track = m_validLoadedTracks[item];
					break;
				}
			}

			if ( track == null )
			{
				foreach ( var item in m_invalidLoadedTracks.Keys )
				{
					if ( mediaLocation.Match( item ) )
					{
						track = m_invalidLoadedTracks[item];
					}
				}
			}

			if ( track != null )
			{
				track.MediaLocation.Merge( mediaLocation );
			}
			else
			{
				track = new MediaTrack( mediaLocation );
			}

			foreach ( var item in MediaIO.GetValidAbsoluteUrisFrom( track.MediaLocation ) )
			{
				m_validLoadedTracks[item] = track;
			}

			m_invalidLoadedTracks[track.MediaLocation] = track;

			return track;
		}

		/// <summary>
		/// Finds the values of the missing properties
		/// </summary>
		protected async Task Init() // TODO: lock down every property while the init is underway
		{
			try
			{
				// TODO: minél több async
				Metadata libraryMeta = null;
				Metadata playerMeta = null;

				if ( MediaPlayer == null )
				{
					MediaPlayer = await Controller.Instance.PluginManager.GetMediaPlayerForFileType( FileExtension );
				}

				if ( LibraryEntry == null )
				{
					// TODO: get entry from library
				}

				// Get metadata from the different locations and merge them
				if ( LibraryEntry != null )
				{
					libraryMeta = LibraryEntry.Metadata;
				}

				if ( MediaPlayer != null )
				{
					playerMeta = await MediaPlayer.GetMetadataForTrack( this );
				}

				if ( TrackMetadata != null )
				{
					TrackMetadata = TrackMetadata.Merge( libraryMeta );
				}
				else
				{
					TrackMetadata = libraryMeta;
				}

				if ( TrackMetadata != null )
				{
					TrackMetadata = TrackMetadata.Merge( playerMeta );
				}
				else
				{
					TrackMetadata = playerMeta;
				}

				if ( TrackMetadata == null )
				{
					TrackMetadata = new Metadata()
					{
						Title = FileName
					};
				}

				if ( FilePath == null )
				{
					Error = new MissingMediaException();
				}
				else if ( MediaPlayer == null )
				{
					Error = new FormatNotSupportedException( FileExtension, (FilePath != null ? FilePath.ToString() : "") );
				}

				IsInitialized = true;
			}
			catch ( Exception e )
			{
				Error = e;
			}
		}

		#endregion Construction and initialization

		/// <summary>
		/// Determines whether the specified Object is equal to the current Object.
		/// </summary>
		/// <param name="o">The Object to compare with the current Object.</param>
		/// <returns>true if the specified Object is equal to the current Object; otherwise, false.</returns>
		public override bool Equals( object o )
		{
			if ( o == null )
			{
				return false;
			}

			if ( object.ReferenceEquals( this, o ) )
			{
				return true;
			}

			var track = o as MediaTrack;

			if ( track == null )
			{
				return false;
			}

			// neither of them are null here
			if ( LibraryEntry == null && track.LibraryEntry == null )
			{
				if ( FilePath != null )
				{
					// if we cannot compare by entries, try the path
					return FilePath.Equals( track.FilePath );
				}
				else
				{
					// If all else fails, suppose they are different
					return false;
				}
			}

			if ( LibraryEntry == null || track.LibraryEntry == null )
			{
				// only one of them is null here
				return false;
			}

			return LibraryEntry.Equals( track.LibraryEntry );
		}

		/// <summary>
		/// Returns a hash code for the current object.
		/// </summary>
		/// <returns>A hash code for the current object.</returns>
		public override int GetHashCode()
		{
			if ( LibraryEntry == null )
			{
				if ( FilePath == null )
				{
					return System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode( this );
				}

				return FilePath.GetHashCode();
			}

			return LibraryEntry.GetHashCode();
		}
	}
}
